////////////////////////////////////////////////////////////////////////////////
//  
//  Copyright (c) 2010 Gabriel Montagné Láscaris-Comneno and Alberto
//  Brealey-Guzmán.
//  
//  Permission is hereby granted, free of charge, to any person obtaining a
//  copy of this software and associated documentation files (the "Software"),
//  to deal in the Software without restriction, including without limitation
//  the rights to use, copy, modify, merge, publish, distribute, sublicense,
//  and/or sell copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following conditions:
//  
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
//  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//  DEALINGS IN THE SOFTWARE.
//  
////////////////////////////////////////////////////////////////////////////////

package com.rojored.controls 
{

import com.rojored.controls.controlBarClasses.ControlBarEvent;
import com.rojored.controls.controlBarClasses.ControlBarSkin;
import com.rojored.controls.progressDisplayClasses.IBufferProgressDisplay;
import com.rojored.controls.progressDisplayClasses.IFractionDisplay;
import com.rojored.controls.progressDisplayClasses.IPlayheadProgressDisplay;
import com.rojored.formatters.TimeFormatter;
import flash.display.InteractiveObject;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.getQualifiedClassName;
import mx.events.FlexEvent;
import mx.logging.ILogger;
import mx.logging.Log;
import spark.components.Label;
import spark.components.ToggleButton;
import spark.components.supportClasses.Range;
import spark.components.supportClasses.SkinnableComponent;

//--------------------------------------
//   Events 
//--------------------------------------

/**
 *  Dispatched to indicate a "play" gesture.
 */
[Event(name="play", type="com.rojored.controls.controlBarClasses.ControlBarEvent")]

/**
 *  Dispatched to indicate a "pause" gesture.
 */
[Event(name="pause", type="com.rojored.controls.controlBarClasses.ControlBarEvent")]

/**
 *  Dispatched to indicate a "scrub" gesture.
 */
[Event(name="scrub", type="com.rojored.controls.controlBarClasses.ControlBarEvent")]

/**
 *  Dispatched to indicate a "volumeChange" gesture.
 */
[Event(name="volumeChange", type="com.rojored.controls.controlBarClasses.ControlBarEvent")]

//--------------------------------------
//   Skin states 
//--------------------------------------

/**
 *  The associated media player is stopped.
 */
[SkinState("stopped")]

/**
 *  The associated media player is playing.
 */
[SkinState("playing")]

/**
 *  The associated media player is paused.
 */
[SkinState("paused")]

/**
 *  The associated media player is buffering.
 */
[SkinState("buffering")]

/**
 *  The associated media player is stopped and full-screen.
 */
[SkinState("stoppedAndFullScreen")]

/**
 *  The associated media player is buffering and full-screen.
 */
[SkinState("bufferingAndFullScreen")]

/**
 *  The associated media player is playing and full-screen.
 */
[SkinState("playingAndFullScreen")]

/**
 *  The associated media player is paused and full-screen.
 */
[SkinState("pausedAndFullScreen")]

/**
 *  Control bar for playing media.
 *
 *  @author gabriel montagné láscaris-comneno gabriel@rojored.com
 */
public class ControlBar extends SkinnableComponent 
{

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor
     */
    public function ControlBar() 
    {
        super();
        setStyle("skinClass", ControlBarSkin);
    }

    //--------------------------------------------------------------------------
    //
    //  Variables 
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  If <code>true</code>, the user is doing a manual scrub; the component
     *  shouldn't update it's value until this is done.
     */
    private var isScrubChanging:Boolean;

    /**
     *  @private
     *  Invalidation flag.
     */
    private var isScrubChangingChanged:Boolean;

    /**
     *  @private
     */
    private var isVolumeChanging:Boolean;

    /**
     *  @private
     *  Invalidation flag
     */
    private var isVolumeChangingChanged:Boolean;

    /**
     *  @private
     */
    private var logger:ILogger =
        Log.getLogger(getQualifiedClassName(this).replace(/\W+/g, ".")); 

    /**
     *  @private
     */
    private var playClicked:Boolean;

    /**
     *  @private
     */
    private var pauseClicked:Boolean;

    /**
     *  @private
     */
    private var scrubRangeChanged:Boolean;

    /**
     *  @private
     */
    private var volumeRangeChanged:Boolean;

    //--------------------------------------------------------------------------
    //
    //  SkinParts 
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    //  scrubRange
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Range used for scrubbing
     */
    public var scrubRange:Range;

    //--------------------------------------
    //  volumeRange 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Range used to change the volume.
     */
    public var volumeRange:Range;

    //--------------------------------------
    //  playPauseButton 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  If selected, will trigger a "play" gesture.  When unselected, will
     *  generate a "pause" gesture.
     */
    public var playPauseButton:ToggleButton;

    //--------------------------------------
    //  pauseButton 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Object which, when clicked, will generate a "pause" gesture
     */
    public var pauseButton:InteractiveObject;

    //--------------------------------------
    //  playButton 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Object which, when clicked, will generate a "play" gesture
     */
    public var playButton:InteractiveObject;

    //--------------------------------------
    //  progressDisplay 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Visualization of the playhead (and optionally buffer) progress.
     */
    public var progressDisplay:IPlayheadProgressDisplay;

    //--------------------------------------
    //  progressLabel 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Text display for the played and total times.
     */
    public var progressLabel:Label;

    //--------------------------------------
    //  volumeFractionDisplay 
    //--------------------------------------

    [SkinPart(required="false")]

    /**
     *  Visualization for the volume level.
     */
    public var volumeFractionDisplay:IFractionDisplay;

    //--------------------------------------------------------------------------
    //
    //  Properties 
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    //  bytesLoaded 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the bytesLoaded property
     */
    private var _bytesLoaded:Number;

    /**
     *  @private
     */
    private var bytesLoadedChanged:Boolean;

    [Bindable("bytesLoadedChanged")]

    /**
     *  @copy com.rojored.controls.progressDisplayClasses.IBufferProgressDisplay#bytesLoaded
     */
    public function get bytesLoaded():Number
    {
        return _bytesLoaded;
    }

    /**
     *  @private
     */
    public function set bytesLoaded(value:Number):void
    {
        if (_bytesLoaded == value)
            return;

        _bytesLoaded = value;
        bytesLoadedChanged = true;

        invalidateProperties();

        dispatchEvent(new Event("bytesLoadedChanged"));
    }

    //--------------------------------------
    //  bytesTotal 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the bytesTotal property
     */
    private var _bytesTotal:Number;

    /**
     *  @private
     */
    private var bytesTotalChanged:Boolean;

    [Bindable("bytesTotalChanged")]

    /**
     *  @copy com.rojored.controls.progressDisplayClasses.IBufferProgressDisplay#bytesTotal
     */
    public function get bytesTotal():Number
    {
        return _bytesTotal;
    }

    /**
     *  @private
     */
    public function set bytesTotal(value:Number):void
    {
        if (_bytesTotal == value)
            return;

        _bytesTotal = value;
        bytesTotalChanged = true;

        invalidateProperties();

        dispatchEvent(new Event("bytesTotalChanged"));
    }

    //--------------------------------------
    //  isFullScreen 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the isFullScreen property
     */
    private var _isFullScreen:Boolean;

    /**
     *  @private
     */
    private var isFullScreenChanged:Boolean;

    [Bindable("isFullScreenChanged")]

    /**
     *  if <code>true</code>, the media player is in full screen mode.
     */
    public function get isFullScreen():Boolean
    {
        return _isFullScreen;
    }

    /**
     *  @private
     */
    public function set isFullScreen(value:Boolean):void
    {
        if (_isFullScreen == value)
            return;

        _isFullScreen = value;
        isFullScreenChanged = true;

        invalidateProperties();
        invalidateSkinState();

        dispatchEvent(new Event("isFullScreenChanged"));
    }

    //--------------------------------------
    //  isPaused 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the isPaused property
     */
    private var _isPaused:Boolean;

    /**
     *  @private
     */
    private var isPausedChanged:Boolean;

    [Bindable("isPausedChanged")]

    /**
     *  if <code>true</code> the media player associated with this control bar
     *  is paused.
     */
    public function get isPaused():Boolean
    {
        return _isPaused;
    }

    /**
     *  @private
     */
    public function set isPaused(value:Boolean):void
    {
        if (_isPaused == value)
            return;

        _isPaused = value;
        isPausedChanged = true;

        invalidateProperties();
        invalidateSkinState();

        dispatchEvent(new Event("isPausedChanged"));
    }

    //--------------------------------------
    //  isBuffering 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the isBuffering property
     */
    private var _isBuffering:Boolean;

    /**
     *  @private
     */
    private var isBufferingChanged:Boolean;

    [Bindable("isBufferingChanged")]

    /**
     *  if <code>true</code>, the media player associated with this component
     *  is buffering.
     */
    public function get isBuffering():Boolean
    {
        return _isBuffering;
    }

    /**
     *  @private
     */
    public function set isBuffering(value:Boolean):void
    {
        if (_isBuffering == value)
            return;

        _isBuffering = value;
        isBufferingChanged = true;

        invalidateProperties();

        dispatchEvent(new Event("isBufferingChanged"));
    }

    //--------------------------------------
    //  isPlaying 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the isPlaying property
     */
    private var _isPlaying:Boolean;

    /**
     *  @private
     */
    private var isPlayingChanged:Boolean;

    [Bindable("isPlayingChanged")]

    /**
     *  if <code>true</code> the media player associated with this control bar
     *  is currently playing.
     */
    public function get isPlaying():Boolean
    {
        return _isPlaying;
    }

    /**
     *  @private
     */
    public function set isPlaying(value:Boolean):void
    {
        if (_isPlaying == value)
            return;

        _isPlaying = value;
        isPlayingChanged = true;

        invalidateProperties();
        invalidateSkinState();

        dispatchEvent(new Event("isPlayingChanged"));
    }

    //--------------------------------------
    //  playheadTime 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the playheadTime property
     */
    private var _playheadTime:Number;

    /**
     *  @private
     */
    private var playheadTimeChanged:Boolean;

    [Bindable("playheadTimeChanged")]

    /**
     *  Playhead position, measured in seconds, since the media starting
     *  playing. 
     */
    public function get playheadTime():Number
    {
        return _playheadTime;
    }

    /**
     *  @private
     */
    public function set playheadTime(value:Number):void
    {
        if (_playheadTime == value)
            return;

        _playheadTime = value;
        playheadTimeChanged = true;

        invalidateProperties();
        dispatchEvent(new Event("playheadTimeChanged"));
    }

    //--------------------------------------
    //  totalTime 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the totalTime property
     */
    private var _totalTime:Number = 0;

    /**
     *  @private
     */
    private var totalTimeChanged:Boolean;

    [Bindable("totalTimeChanged")]

    /**
     *  Total length of the media, in milliseconds.
     */
    public function get totalTime():Number
    {
        return _totalTime;
    }

    /**
     *  @private
     */
    public function set totalTime(value:Number):void
    {
        if (_totalTime == value)
            return;

        _totalTime = value;
        totalTimeChanged = true;

        invalidateProperties();
        dispatchEvent(new Event("totalTimeChanged"));

    }

    //--------------------------------------
    //  progressLabelFunction 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the progressLabelFunction property
     */
    private var _progressLabelFunction:Function = 
        new TimeFormatter().format;

    /**
     *  @private
     */
    private var progressLabelFunctionChanged:Boolean;

    /**
     *  Function used to format the playheadTime for the progressLabell
     */
    public function get progressLabelFunction():Function
    {
        return _progressLabelFunction;
    }

    /**
     *  @private
     */
    public function set progressLabelFunction(value:Function):void
    {
        if (_progressLabelFunction == value)
            return;

        _progressLabelFunction = value;
        progressLabelFunctionChanged = true;
        invalidateProperties();
    }

    //--------------------------------------
    //  volume 
    //--------------------------------------

    /**
     *  @private
     *  Storage for the volume property
     */
    private var _volume:Number;

    /**
     *  @private
     */
    private var volumeChanged:Boolean;

    [Bindable("volumeChanged")]

    /**
     *  The volume of the media player.  Ranges from 0, silent, to  1, full
     *  volume.
     */
    public function get volume():Number
    {
        return _volume;
    }

    /**
     *  @private
     */
    public function set volume(value:Number):void
    {
        if (_volume == value)
            return;

        _volume = value;
        volumeChanged = true;
        invalidateProperties();
        dispatchEvent(new Event("volumeChanged"));
    }

    //--------------------------------------------------------------------------
    //
    //  Overriden methods: SkinnableComponent
    //  
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function partAdded(partName:String,
                                          instance:Object):void
    {
        super.partAdded(partName, instance);
        if (instance == scrubRange)
        {
            scrubRange.addEventListener(
                Event.CHANGE,
                scrubRange_changeHandler
            );
            scrubRange.addEventListener(
                FlexEvent.CHANGE_START,
                scrubRange_changeStartHandler
            );
            scrubRange.addEventListener(
                FlexEvent.CHANGE_END,
                scrubRange_changeEndHandler
            );
            updatePlayheadTime();
            updateTotalTime();
        }
        else if (instance == volumeRange)
        {
            volumeRange.addEventListener(
                Event.CHANGE,
                volumeRange_changeHandler
            );
            volumeRange.addEventListener(
                FlexEvent.CHANGE_START,
                volumeRange_changeStartHandler
            );
            volumeRange.addEventListener(
                FlexEvent.CHANGE_END,
                volumeRange_changeEndHandler
            );
            volumeRange.minimum = 0;
            volumeRange.maximum = 1;
        }
        else if (instance == playPauseButton)
        {
            playPauseButton.addEventListener(
                Event.CHANGE,
                playPauseButton_changeHandler
            );
            playPauseButton.selected = isPlaying;
        }
        else if (instance == playButton)
        {
            playButton.addEventListener(
                MouseEvent.CLICK,
                playButton_clickHandler
            );
        }
        else if (instance == pauseButton)
        {
            pauseButton.addEventListener(
                MouseEvent.CLICK,
                pauseButton_clickHandler
            );
        }
        else if (instance == progressDisplay)
        {
            updatePlayheadTime();
            updateTotalTime();
            updateBytesLoaded();
            updateBytesTotal();
        }
    }

    /**
     *  @private
     */
    override protected function partRemoved(partName:String,
                                            instance:Object):void 
    {
        super.partRemoved(partName, instance);
        if (instance == scrubRange)
        {
            scrubRange.removeEventListener(
                Event.CHANGE,
                scrubRange_changeHandler
            );
            scrubRange.removeEventListener(
                FlexEvent.CHANGE_START,
                scrubRange_changeStartHandler
            );
            scrubRange.removeEventListener(
                FlexEvent.CHANGE_END,
                scrubRange_changeEndHandler
            );
        }
        else if (instance == volumeRange)
        {
            volumeRange.removeEventListener(
                Event.CHANGE,
                volumeRange_changeHandler
            );
        }
        else if (instance == playButton)
        {
            playButton.removeEventListener(
                MouseEvent.CLICK,
                playButton_clickHandler
            );
        }
        else if (instance == pauseButton)
        {
            pauseButton.removeEventListener(
                MouseEvent.CLICK,
                pauseButton_clickHandler
            );
        }
    }

    /**
     *  @private
     */
    override protected function getCurrentSkinState():String
    {

        var skinState:String = 
                isPlaying   ? "playing"
            :   isPaused    ? "paused"
            :   isBuffering ? "buffering"
            :   "stopped"
            ;

        if (isFullScreen)
            skinState += "AndFullScreen";

        return skinState;
    }

    //--------------------------------------------------------------------------
    //
    //  Overriden methods: UIComponent 
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function commitProperties():void
    {
        super.commitProperties();

        if (isPlayingChanged)
        {
            isPlayingChanged = false;
            if (playPauseButton)
                playPauseButton.selected = isPlaying;
        }

        if (totalTimeChanged)
        {
            totalTimeChanged = false;
            updateTotalTime();
        }

        if (scrubRangeChanged)
        {
            scrubRangeChanged = false;
            var scrubEvent:ControlBarEvent = 
                new ControlBarEvent(ControlBarEvent.SCRUB);
            scrubEvent.playheadTime = scrubRange.value;
            dispatchEvent(scrubEvent);
        }

        if (volumeRangeChanged)
        {
            volumeRangeChanged = false;
            var volumeChangeEvent:ControlBarEvent = new
                ControlBarEvent(ControlBarEvent.VOLUME_CHANGE);
            volumeChangeEvent.volume = volumeRange.value;
            dispatchEvent(volumeChangeEvent);
        }

        if (playheadTimeChanged || isScrubChangingChanged ||
            progressLabelFunctionChanged)
        {
            playheadTimeChanged = false;
            isScrubChangingChanged = false;
            progressLabelFunctionChanged = false;
            updatePlayheadTime();
        }

        if (bytesTotalChanged)
        {
            bytesTotalChanged = false;
            updateBytesTotal();
        }

        if (bytesLoadedChanged)
        {
            bytesLoadedChanged = false;
            updateBytesLoaded();
        }

        if (playClicked)
        {
            playClicked = false;
            if (!isPlaying)
                dispatchEvent(new ControlBarEvent(ControlBarEvent.PLAY));
        }

        if (pauseClicked)
        {
            pauseClicked = false;
            if (!isPaused)
            {
                dispatchEvent(new ControlBarEvent(ControlBarEvent.PAUSE));
            }
        }

        if (volumeChanged || isVolumeChangingChanged)
        {
            volumeChanged = false;
            isVolumeChangingChanged = false;
            updateVolume();
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Methods 
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  updates the playehad value on the different skin parts.
     */
    private function updatePlayheadTime():void
    {
        if (progressDisplay)
            progressDisplay.playheadTime = 
                    !isScrubChanging ? playheadTime
                :   scrubRange       ? scrubRange.value
                :   progressDisplay.playheadTime
                ;

        if (progressLabel && progressLabelFunction != null)
            progressLabel.text = 
                progressLabelFunction((!isScrubChanging ? playheadTime :
                    scrubRange ? scrubRange.value : 0)); 

        if (scrubRange && !isScrubChanging)
            scrubRange.value = playheadTime;
    }

    /**
     *  @private
     *  Update the skin parts that are used to handle and visualize media
     *  player's volume.
     */
    private function updateVolume():void
    {
        logger.info("updateVolume: " + volume);
        if (volumeFractionDisplay)
            volumeFractionDisplay.fraction = 
                !isVolumeChanging   ? volume 
            :   volumeRange         ? volumeRange.value
            :   volumeFractionDisplay.fraction
            ;

        if (volumeRange && !isVolumeChanging)
            volumeRange.value = volume;
    }

    /**
     *  @private
     */
    private function updateTotalTime():void
    {
        if (scrubRange)
            scrubRange.maximum = totalTime;

        if (progressDisplay)
            progressDisplay.totalTime = totalTime;
    }

    /**
     *  @private
     */
    private function updateBytesTotal():void
    {
        if (progressDisplay && progressDisplay is IBufferProgressDisplay)
            IBufferProgressDisplay(progressDisplay).bytesTotal = bytesTotal;
    }

    /**
     *  @private
     */
    private function updateBytesLoaded():void
    {
        if (progressDisplay && progressDisplay is IBufferProgressDisplay)
            IBufferProgressDisplay(progressDisplay).bytesLoaded = bytesLoaded;
    }

    //--------------------------------------------------------------------------
    //
    //  Event handlers 
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Change based on user interaction.
     */
    private function scrubRange_changeHandler(event:Event):void
    {
        logger.info("scrubRangeChanged");
        scrubRangeChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function scrubRange_changeEndHandler(event:FlexEvent):void
    {
        logger.info("scrubRange_changeEndHandler");
        if (!isScrubChanging)
            return;

        isScrubChanging = false;
        isScrubChangingChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function scrubRange_changeStartHandler(event:FlexEvent):void
    {
        logger.info("scrubRange_changeStartHandler");
        if (isScrubChanging)
            return;

        isScrubChanging = true;
        isScrubChangingChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     *  Change based on user interaction.
     */
    private function volumeRange_changeHandler(event:Event):void
    {
        logger.info("volumeChanged: " + volumeRange.value);
        volumeRangeChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function volumeRange_changeStartHandler(event:FlexEvent):void
    {
        logger.debug("volumeRange_changeStartHandler: " + volumeRange.value);
        if (isVolumeChanging)
            return;

        isVolumeChanging = true;
        isVolumeChangingChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function volumeRange_changeEndHandler(event:FlexEvent):void
    {
        logger.debug("volumeRange_changeEndHandler: " + volumeRange.value);
        if (!isVolumeChanging)
            return;

        isVolumeChanging = false;
        isVolumeChangingChanged = true;
        invalidateProperties();
    }

    /**
     *  @private
     *  Change based on user interaction.
     */
    private function playPauseButton_changeHandler(event:Event):void
    {
        logger.info("play pause button changed");
        if (playPauseButton.selected)
            playClicked = true;
        else
            pauseClicked = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function playButton_clickHandler(event:MouseEvent):void
    {
        playClicked = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function pauseButton_clickHandler(event:MouseEvent):void
    {
        pauseClicked = true;
        invalidateProperties();
    }
}
}
